package pt.compflow.interfaces;

import java.io.InputStream;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import pt.compflow.Context;
import pt.compflow.flowontology.model.Interface;
import pt.compflow.storage.TripleStoreConnection;

import com.hp.hpl.jena.ontology.OntClass;
import com.hp.hpl.jena.ontology.OntModel;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.ModelFactory;
import com.hp.hpl.jena.rdf.model.RDFNode;
import com.hp.hpl.jena.rdf.model.Resource;
import com.hp.hpl.jena.rdf.model.Statement;
import com.hp.hpl.jena.rdf.model.StmtIterator;
import com.hp.hpl.jena.util.iterator.ExtendedIterator;
import com.hp.hpl.jena.vocabulary.RDF;

/**
 * InterfaceManager Maps an Interface OntClass (subclass of Interface) to 
 * Java interface classes.
 * 
 * The underlying subClassOf relationships between OntClasses are used
 * so that a Java class mapped to a top-level class is also mapped to
 * all its sub-classes, unless a sub-class specific map exists. 
 * 
 * The interfaceMap maintains a registry of all available Interface classes.
 * These are instances to avoid reflection errors during workflow execution time;
 * reflection errors will occur during initialization of the engine, if there are any.
 * 
 * The instanceMap contains all Interface instances in use by the engine. This is
 * a cache, meaning that unused instances will be removed after strong references to them
 * cease to exist for some time (become garbage collected).
 */
public class InterfaceManager<I extends IOInterface> {
	
	private static final Logger LOG = LoggerFactory.getLogger(InterfaceManager.class);

	private Map<String, IOInterface> interfaceMap;	
	
	private Map<String, SoftReference<IOInterface>> instanceMap;
	// the ReferenceQueue will save EngineInterfaces collected by the garbage collector
	private ReferenceQueue<IOInterface> deadReferenceQueue;
	// the revMap allows us to get the instanceMap key, after getting a dead reference in the queue
	private Map<SoftReference<IOInterface>, String> revMap;
	
	private List<InterfaceListener> listeners;
	
	public InterfaceManager() {
		interfaceMap = new HashMap<String, IOInterface>();
		
		instanceMap = new HashMap<String, SoftReference<IOInterface>>();
		revMap = new HashMap<SoftReference<IOInterface>, String>();
		deadReferenceQueue = new ReferenceQueue<IOInterface>();
		
		listeners = new ArrayList<InterfaceListener>();
	}
	
	/**
	 * Removes garbage collected EngineInterfaces from the Manager instances
	 * These are, typically, EngineInterfaces without strong references to them
	 * which haven't been used for a while
	 */
	public void cleanup(Context context, TripleStoreConnection store) {
		Reference<? extends IOInterface> deadInterface = null;
		
		while((deadInterface = deadReferenceQueue.poll()) != null) {
			IOInterface ioi = deadInterface.get();
			if(ioi != null) ioi.stop(context, store);
			else LOG.warn("Cannot stop interface since garbage collector has already removed the instance! FIXME!");
			String key = revMap.get(deadInterface);
			revMap.remove(deadInterface);
			instanceMap.remove(key);
		}
	}
		
	/**
	 * Registers a mapping between the given java input/output interface classes path/name and the given Interface OntClass
	 * 
	 */
	@SuppressWarnings("unchecked")
	public IOInterface register(String interfaceClassPath, Resource interfaceClass) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchAlgorithmException {
		if(interfaceClassPath == null || interfaceClass == null) return null;		
		Class<? extends I> clazz = (Class<? extends I>) Class.forName(interfaceClassPath);
		return register(clazz, interfaceClass);
	}
	
	public IOInterface register(Class<? extends I> clazz, Resource interfaceClass) throws InstantiationException, IllegalAccessException, NoSuchAlgorithmException {
		if(clazz == null || interfaceClass == null) return null;					
		IOInterface ei = clazz.newInstance(); 
		ei.setInterfaceClassUri(interfaceClass.getURI());
		ei.setInterfaceListeners(listeners);
		interfaceMap.put(interfaceClass.getURI(), ei);	
		return ei;
	}
	
	/**
	 * Unregisters a mapping for the given Interface OntClass
	 * 
	 * @param interfaceClass
	 * @return
	 * @throws NoSuchAlgorithmException
	 */
	public boolean unregister(Resource interfaceClass) throws NoSuchAlgorithmException {
		if(interfaceClass == null) return false;
		return interfaceMap.remove(interfaceClass.getURI()) != null;
	}
		
	/**
	 * Get a mapped EngineInterface Java class instance for the supplied Interface OntClass and instance, if found
	 * If the instance is already present in the instanceMap, its EngineInterface is returned;
	 * Otherwise, a new EngineInterface instance is created, if found in the interfaceMap.
	 * @param interfaze - the interface instance
	 * @param interfaceClass - the interface instance type
	 * @param ontModel - the ontology model describing the interface instance
	 * @return instance of the mapped java class, if found, or null if not found
	 */
	public IOInterface get(Interface interfaze, Resource interfaceClass, OntModel ontModel, Context context, TripleStoreConnection store) {

		cleanup(context, store);
		
		if(interfaze == null) return null;
		
		if(instanceMap.containsKey(interfaze.getURI()))
			return instanceMap.get(interfaze.getURI()).get();
						
		// if a direct class is found return...		
		if(interfaceMap.containsKey(interfaceClass.getURI())) {
			IOInterface engineInterface = interfaceMap.get(interfaceClass.getURI()).copy();
			engineInterface.setInterfaceUri(interfaze.getURI());
			SoftReference<IOInterface> ref = new SoftReference<IOInterface>(engineInterface, deadReferenceQueue);
			instanceMap.put(interfaze.getURI(), ref);
			revMap.put(ref, interfaze.getURI());
			engineInterface.start(context, store);
			return engineInterface;
		}
		
		if(ontModel == null) {
			LOG.warn("Cannot check parent class mappings since the OntModel is NULL!");
			return null;
		}
				
		List<Resource> classList = new ArrayList<Resource>();
		classList.add(interfaceClass);
		List<Resource> totalClassList = new ArrayList<Resource>();
		totalClassList.add(interfaceClass);
		
		while(classList.size() > 0) {
			List<Resource> superList = new ArrayList<Resource>();
			for(Resource clazz : classList) {
				ExtendedIterator<OntClass> superClassIt = ontModel.getOntClass(clazz.getURI()).listSuperClasses(true);
				while(superClassIt.hasNext()) {
					OntClass superClass = superClassIt.next();
					if(superClass.isURIResource() && !totalClassList.contains(superClass)) {
						if(interfaceMap.containsKey(superClass.getURI())) {
							IOInterface interfaceGroup = interfaceMap.get(superClass.getURI()).copy();
							interfaceGroup.setInterfaceUri(interfaze.getURI());
							instanceMap.put(interfaze.getURI(), new SoftReference<IOInterface>(interfaceGroup, deadReferenceQueue));
							interfaceGroup.start(context, store);
							return interfaceGroup;
						}
						
						superList.add(superClass);
						totalClassList.add(superClass);
					}
				}
			}
			classList = superList;
		}
		
		return null;
	}
	
	/**
	 * Loads class mappings from an N3 file according to the http://compflow.pt/config vocabulary.
	 * @param f - the N3 file
	 * @throws IllegalAccessException 
	 * @throws InstantiationException 
	 * @throws ClassNotFoundException 
	 * @throws NoSuchAlgorithmException 
	 */
	public void loadFromN3(InputStream stream) throws NoSuchAlgorithmException, ClassNotFoundException, InstantiationException, IllegalAccessException {
			
		// this is a good example of how confusing it can be to read data with the base model class of Jena
		// you have to think in terms of a graph with statements (triples), properties, nodes, etc. 
		
		// 1. load the N3 configuration file into a default Jena Model
		Model m = ModelFactory.createDefaultModel();
		m.read(stream, null, "N3");
		
		// 2. load all mappings in the configuration file
		// list all statements/triples with predicate RDF.type and object #ClassMap
		// i.e., list all subjects of type #ClassMap (something like a "listClassMaps" if a specific model existed)
		StmtIterator stmtIt = m.listStatements((Resource) null, RDF.type,  m.getResource("http://compflow.pt/config#InterfaceMap"));
		while(stmtIt.hasNext()) {
			Statement stm = stmtIt.next();
			
			// get the actual subject of type #ClassMap
			Resource map = stm.getSubject();
			// get the code value (the java class location and name) associated with this subject (map)
			RDFNode javaClassRes = map.getProperty(m.getProperty("http://compflow.pt/config#code")).getObject();
			// get the ontClasses value associated with this subject (map)
			Resource interfaceOntClassRes = map.getPropertyResourceValue(m.getProperty("http://compflow.pt/config#interfaceOntClass"));
	
			
			// the ontClasses should be a RDFList
			if(!javaClassRes.isLiteral()) 
				throw new RuntimeException("Invalid configuration file");
			
			String javaClassPath = javaClassRes.asLiteral().getString();
			Resource interfaceOntClass = m.getResource(interfaceOntClassRes.getURI());
	
			// register the mapping
			register(javaClassPath, interfaceOntClass);
		}
			
	}
	
	public void addInterfaceListener(InterfaceListener l) {
		if (l == null || listeners.contains(l)) return;
		listeners.add(l);
	}

	public void removeInterfaceListener(InterfaceListener l) {
		if (l == null || !listeners.contains(l)) return;
		listeners.remove(l);
	}
	
}
